Skip to content

Extract a plot_types/ package — one module per plot type (#83, #91) - #93

Merged
AlexisJanin merged 11 commits into
mainfrom
plot-types-package
Aug 28, 2026
Merged

Extract a plot_types/ package — one module per plot type (#83, #91)#93
AlexisJanin merged 11 commits into
mainfrom
plot-types-package

Conversation

@AlexisJanin

@AlexisJanin AlexisJanin commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Implements #83 (a plot_types/ package, one module per plot type) and its follow-up #91 (other stops knowing what a spectrogram is).

The decision

A plot type is a module. Everything that varies by plot type lives in that type's package; no other module branches on plot type.

The failure mode being designed against is the one PSD hit: a plot type that validates cleanly and renders nothing, because one of six spellings of the derived-plot list was forgotten. Those six spellings are now one registry.

What moved

From To
Signal.loop_from_signals / spectrogram_from_signal / psd_from_signal plot_types/<name>/plot.py
plot_assembly's builders, qualifiers and _DERIVED_PLOTS plot_types/registry.py + each definition.py
constants.PlotType, its capability tuples and its import-time guard plot_types/registry.py
constants.SpectrogramConfig / PsdConfig plot_types/<name>/definition.py
database_options_xlsx's three sheet blocks + two psd helpers each definition.py
database_options_parser's _check_spectral_types / _check_psd_entries each definition.py's validate()
other/find_load_format.PER_FILE_DERIVED_SECTIONS + its 3 qualifiers deleted — plot_assembly desugars both scopes (#91)
ValidationIssue validation.py, a leaf both halves can import
reference resolution signal_reference.py
PlotModel.assign_plot_model plot_assembly.assemble_plot_models

Everything a plot type knows travels on the object

A Signal carries its definition (plot_options.definition) and its RenderSpec. Every render site reads the flag off that object:

if self.definition.GRID_LAYOUT:      # not: if self.plot_type in registry.GRID_LAYOUT

So signal_container imports nothing from plot_types but base, and the data model never knows the roster. plot_type survives as a read-only property returning definition.NAME, for logs, figure titles and Dash stores.

registry.definition_for(name) is the single exception, at the one boundary where the object could not be carried: a plot type that crossed a Dash store as JSON has only its name left. An unregistered name resolves to Unknown, every capability off — deliberately not the time_series defaults, which would let a typo render plausibly.

This replaces what the first four commits did, and reverses a conclusion this PR previously stated. They kept capabilities as registry-side frozensets keyed by name, because signal_container is reachable from a half-initialised datasource package and so must not import a plot.py. That constraint was real on the dependency graph but never fired: clinical_scope/__init__.py eagerly imports wrapper, which fixes module order for every entry point, and signal_container bound the registry as a module object, touching attributes only at call time. Both facts were already visible in the earlier measurement — "Both designs import fine through the normal entry point" — which produced its ImportError only after separately modifying the tree to settle the datasource cycle first.

The cost was never the cycle. It was that one question had two shapes — rendering pushed, capabilities pulled — and the string-keyed one grew hand-maintained duplicates. PlotOptions.square_plot was a second spelling of GRID_LAYOUT kept in sync by nothing: the fake plot type in the test suite declared one and not the other, and rendered full-width.

The two halves per package

  • definition.py — what the type is: NAME/SECTION_KEY, the six capability flags, config keys, validate(), map_refs(), xlsx sheet + row interpretation. Imports nothing but validation.
  • plot.py — what it does: build(), the maths, the rendering it installs. Imports signal_container, numpy and plotly.

The split is stylistic, not load-bearing — reversing this PR's earlier claim. With the cycle gone the layering argument went too, and it was measured rather than assumed:

>>> import clinical_scope.database_options_parser
heavy modules pulled in: ['plotly', 'numpy']
plot.py modules loaded : [loop.plot, psd.plot, spectrogram.plot]

No entry point ever saw a config layer that loads without plotly. What the split earns now is readability and consistency: psd is 302 lines of spreadsheet row accumulation and group resolution against 154 lines of a Welch call and axis wiring, and it keeps the shape a registered module already has here — datasource/sources/<name>/ is options.py plus find_load_format.py, the same declarative half and working half.

registry.py and builders.py, split for the same dead reason, were merged. time_series is registered but has no package — every default in PlotTypeDefinition is its behaviour, and DERIVED is the types with a SECTION_KEY.

Behaviour changes

  • loop gains config validation. It had none — wrong arity, a non-list entry and a non-string member were all silent, and a loop naming one signal simply failed to appear.
  • A malformed per-file other::<stem> loop now costs one plot instead of the whole file.
  • spectrogram and psd now report a non-dict entry instead of returning no issue, which was the validates-cleanly-renders-nothing failure surviving inside the package. loop already reported it.
  • Two Dash sites that compared == LOOP now read a sixth capability, POINT_TIMESTAMPS.
  • KNOWN_SECTION_KEYS declares only the keys no plot type owns; the parser unions each registered type's own key onto it, so a new plot type can no longer make a valid config warn "Unknown key".
  • A grid plot type with a single subplot now gets its square width from GRID_LAYOUT. Loops always set both flags, so nothing visible changes for them — but a future grid type can no longer half-declare itself.

Acceptance criteria

  • No module outside plot_types/ branches on plot type or hardcodes a derived section key — asserted by tests/plot_types/test_boundaries.py (AST).
  • signal_container imports nothing from plot_types but base — same test, strengthened from the original "imports no plot.py".
  • Adding a plot type = one package + two adjacent lines in registry.py (AVAILABLE and BUILDERS). Qualified — see Scope of the "nothing changes" claim.
  • A forgotten piece is an import-time crash, never a config that validates and renders nothing — tests/plot_types/test_fake_plot_type.py registers a fourth type and drives it through all six paths.
  • loop gains the config validation the other two have.
  • Snapshot tests unchanged — no figure moves.
  • other/find_load_format.py imports nothing from plot_types (Collapse other's per-file reference scoping into plot_assembly #91).
  • Both config spellings stay valid — no parser change, no config migration.

Scope of the "nothing changes" claim

True of how a plot type behaves. A type wanting a user display setting still needs a UserOptions class in constants.py plus a DisplayFallbacks field (as loops_per_row and spectrogram_db_range do), and one wanting an axis payload of its own needs a field on Data (as point_time_axis and spectrogram_freq_axis do). FakeDefinition has neither, so the test proving the claim does not cover that half. CLAUDE.md and registry.py say so.

Why definition and not schema

"Schema" described one third of the file. Alongside config grammar it carries the type's identity and its six capability flags — booleans about how the type draws, and the file's most-read content. The word was also already spent twice: cst.UserOptions.LoopsPerRow and its siblings are schema classes, and data_callbacks builds a Dash schema-registry mapping component ids to widget schemas. Three meanings; the plot type had the weakest claim.

self.definition.GRID_LAYOUT reads as "this plot type's definition says it is a grid", where self.schema.GRID_LAYOUT read as a category error. The rename was not applied blindly — the other two meanings were left alone, and sentences it would have degraded were rewritten ("the sheet columns and the JSON keys are one schema in two spellings" was right the first time, and became "one grammar in two spellings").

Commits

  1. 52fcad7 — rendering into a package of its own (the risky half; guarded by the snapshot suite)
  2. e708a44 — config into its own package (mechanical; guarded by the config/validation tests)
  3. 98dffe7other stops referencing plot types (Collapse other's per-file reference scoping into plot_assembly #91)
  4. 01d83a8 — three-axis review follow-ups
  5. c0c349b — guard the periphery a plot type lands in (demo config + docs)
  6. ef8297f — a /new-plot-type skill
  7. 6e44a34 — comments stating current behaviour, not what the branch changed
  8. ca9b724 — capabilities travel on the object
  9. 9a39124 — fold the builders back into the registry
  10. c9164de — rename the schema half to the definition half

Review follow-ups (ca9b724 onward)

A fourth review pass, on readability and maintainability rather than correctness. Verified against the code before being acted on:

  • data_callbacks read plot_model.name for RESAMPLED while reading plot_model.plot_type for POINT_TIMESTAMPS. They agreed only because __post_init__ aliases name to the plot type — and name is also a Dash component id, so giving a PlotModel a real title would have silently broken resampling.
  • Data.loop_time_axispoint_time_axis, matching the POINT_TIMESTAMPS capability that gates it. data_callbacks reads it inside a branch on that generic flag, so a second timestamped type would have had to populate a field named after loops.
  • The schema/builder seam was typed Any throughout. PlotBuilder.build and read_sheet now have real signatures behind if TYPE_CHECKING, which never executes and so closes no cycle. entries/entry/config stay Any and say why: raw user JSON, and narrowing them is validate()'s job.
  • CellReader gains text() and pair(), absorbing 15 str(row.get()).strip() copies and the db_min/db_max rule that was written twice with two different messages. ValidationIssue.unknown_keys replaces six hand-rolled copies of one message. The row loop is deliberately not unified — psd's rows fan out into several groups, so a shared template would have one and a half users.
  • The comments in 6e44a34 were the same class of finding: several stated what the branch changed ("which is what let psd validate cleanly and render nothing before this") rather than what the code does, which is unreadable to anyone who did not review the diff.

Known, deliberately left

  • A malformed derived section (loop: [], as opposed to a malformed entry) still raises into _flatten_config's handler and skips the whole namespace, taking its grouped_fields with it. Matches pre-existing behaviour, so not a regression — worth its own change.
  • Four maintainability items from the last review, none affecting behaviour: _add_derived_plot_group takes six parameters for one call site; PlotTypeArityError and SourceSignalNotFoundError live in plot_types/base but are raised by loop/plot.py and signal_reference; DisplayFallbacks still carries three type-named fields; registry still flattens AVAILABLE into five collections, which the fake-type fixture restates.

Verification

1107 tests pass, ruff clean. Snapshot tests are in that count (16, not deselected), so no figure moved. The demo database_options.json still regenerates byte-identical from its .xlsx.

The one changed area with no automated coverage is data_callbacks._build_graphs — its only caller is process_visualization, whose test never reaches it. The resampler wrap, the loop time-range slider and a single-subplot grid plot were checked by hand in the running app.

🤖 Generated with Claude Code

alexisj-inria and others added 3 commits August 27, 2026 13:31
A plot type is a module. `loop_from_signals`, `spectrogram_from_signal` and
`psd_from_signal` leave `Signal`, the derived-plot builders leave
`plot_assembly`, and the capability tuples leave `constants.py` -- each into
`plot_types/<name>/`, mirroring `datasource/sources/<name>/`.

The split inside a package is by import-reachability, not by declarative vs
machinery: `schema.py` is a leaf every layer may import, `plot.py` sits at the
top and builds Signals. Capabilities are pure booleans yet live in the leaf
half, because `signal_container` reads them and may never import a `plot.py` --
it is reachable from a half-initialised `datasource` package, so a `plot.py`
importing `Signal` back out of it raises ImportError for some entry points and
not others.

The same constraint splits the registry in two. `registry.py` imports schemas
only, so `signal_container` and the config readers can read capabilities and
PAGE_ORDER freely; `builders.py` imports the plot halves and is read by
`plot_assembly` alone. It is also why rendering is pushed onto a Signal at
construction (`RenderSpec`) rather than pulled at draw time: `to_plotly_trace`
loses its four-branch if/elif on plot type and reads what the builder installed.

`cst.PlotType` and its import-time guard are gone; the registry inherits the
guard's job and probes for a missing `plot.py` with `find_spec` rather than
importing it. Two Dash sites that compared `== LOOP` outright now read a sixth
capability, POINT_TIMESTAMPS -- both mean "points carry a timestamp although x
is not time", one reading it from hover customdata and one from
`loop_time_axis`.

Stage 1 of #83; the config half (validation, xlsx interpretation, map_refs)
follows. No figure moves: the snapshot suite is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second half of the split: config keys, validation, reference rewriting
and xlsx row interpretation join the capabilities in `plot_types/<name>/
schema.py`. `constants.py` gives up `SpectrogramConfig`, `PsdConfig` and the
three derived section keys; `KNOWN_SECTION_KEYS` declares only the keys no plot
type owns, and the parser unions each registered type's own section key onto it,
so a new plot type can no longer make a valid config warn "Unknown key".

Six spellings of the derived-plot list become one. `plot_assembly`'s three
qualifiers and `other`'s three were the same shape-walks over the same shapes,
differing only in the leaf op -- resolve-then-prefix `datasource::` versus
blind-prefix `<stem>::`. Each shape is now walked once, by the type that owns
it, through `map_refs(config, map_ref)`.

The xlsx reader transcribes and the plot type interprets: sheet name, required
columns and row->config mapping move to `schema.py`, so the spreadsheet columns
and the JSON keys -- one schema in two spellings -- are declared where they
cannot drift apart. The cell coercions stay in the reader and are lent to a
schema through `CellReader`, since the reader imports every schema to find its
sheet.

`loop` gains the validation the other two had. It had none, which is why it was
absent from the parser's lists rather than merely forgotten: wrong arity, a
non-list entry and a non-string member were all silent before, and a loop
naming one signal simply failed to appear. A malformed per-file loop now costs
one plot instead of the whole file -- scoping it used to assume a list and
raise inside `other`'s per-file handler.

Three import-time snapshots are gone, found by the fake-plot-type test:
`plot_assembly` bound `BUILDERS` by value, and both the section-key set and the
derived-plot tuple were computed at import. Each was a second source of truth
for a collection the registry owns.

Closes the work of #83. 1060 tests pass; the demo database_options.json still
regenerates byte-identical from its .xlsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alexisj-inria and others added 4 commits August 27, 2026 16:19
Dead on arrival, both added by this branch: `CellReader.is_truthy` was lent to
every schema and read by none -- the two real callers use `_is_truthy` directly
inside the xlsx reader -- and `registry.schema_for` had no caller anywhere.

The capability roster was stated twice. Each flag's *value* was already derived
from the schemas, but the list of flags was written out again as six frozenset
lines, so a seventh capability added to `PlotTypeSchema` and missed here would
be declared and read by nothing -- the silent acceptance this package exists to
kill, one level up. `base.CAPABILITIES` is now the roster and the registry guard
refuses to import a flag no set exposes. The six sets stay written out rather
than generated: deriving them would fix the duplication by hiding `GRID_LAYOUT`
from every reader and every tool, which is the worse trade.

`loop_from_signals` hand-rolled the time-series check that `require_time_series`
already does, and that psd, spectrogram and the fake plot type all call.

The headline claim was true of behaviour and overstated as written. A plot type
wanting a user display setting or an axis payload of its own still pays for the
mechanism carrying it -- a `UserOptions` class, a `DisplayFallbacks` field, a
`Data` field -- as `loops_per_row`, `spectrogram_db_range`, `loop_time_axis` and
`spectrogram_freq_axis` each do, and two of the three real derived types have
one. `FakeSchema` has neither, so the test proving the claim never covered that
half. CLAUDE.md and the registry docstring now say so, and `test_boundaries`
records the two things its AST walk cannot see, so a green run is not read as a
stronger guarantee than it is.

Also: `test_plot_assembly` asserted `cst.DatabaseOptions.FILES` beside literal
"loop" and "grouped_fields"; a docstring in `signal_container` still pointed at
`loop_from_signals()` by name; three comments pruned or tightened.

Verified separately, not in the diff: merging `schema.py` into `plot.py` really
does close the import cycle. With the datasource cycle settled, a registry that
pulls a plot half fails at `loop/plot.py` with "cannot import name 'Data' from
partially initialized module clinical_scope.signal_container", while the split
imports cleanly on the identical sequence -- and both import fine through the
normal entry point, which is the "some entry points and not others" the split
exists to prevent. That answers the open box in #91: the halves stay split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The registry already refuses a plot type missing half its code, and the
snapshot suite covers what it draws. Neither notices a type that imports,
validates, renders — and is configured nowhere and described nowhere.

Two guards for that gap, both green on the three existing types:

- every registry.DERIVED type is configured in the demo config, so a new
  type ships exercised against demo_patient rather than only in unit tests
- every registry.DERIVED type has a tutorial heading and a CONTEXT.md
  glossary term

The doc guard is a weaker category than its neighbours in
test_example_assets.py, which only ever compare registry-derived sets
against disk-derived ones. It anchors on headings and bold glossary terms
rather than a search of the prose: "loop" appears throughout the tutorial
as the datasource loop, a loop subplot's height, multi-cycle loops — so a
body search would report green for a type nobody had documented. The
accepted spellings come off the schema (NAME, SECTION_KEY, SHEET_NAME),
which is what lets `loops` match `loop` without hardcoding a plural.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors /new-datasource, but with a different job. That skill is largely a
checklist because a datasource has coordinated pieces easy to forget; here
the registry already makes a forgotten half an import-time crash, so a
checklist would be guarding a door that is already locked.

What the code cannot decide is what the skill covers:

- the gate. A plot type earns a package only by declaring a *delta* from
  time_series. Four flags discriminate — TIME_AXIS, GRID_LAYOUT,
  HAS_COLORBAR, POINT_TIMESTAMPS. RESAMPLED and UNIFIED_HOVER are False on
  all three existing types, so they say "I am derived" rather than what
  makes one different, and gating on them would accept anything. The gate
  falls out of the classification rather than preceding it: the four
  answers that pass it are the four that configure the scaffold.

- the maths, as a blocking checkpoint. plot.py is an adapter —
  spectrogram_from_signal calls spectral.spectrogram() and spends its body
  wrapping the result — so the skill scaffolds the adapter and never
  invents the transform. Declaring the refusal exception is required:
  plot_assembly grades an undeclared one as a crash with a full traceback.

When no flag is a delta the skill names the hole and stops rather than
routing anywhere. New maths drawn against time, sharing a zoom with its
source, has no home today — every builder sets its own plot_type=, and
nothing builds a derived Signal that renders as a time-series. Forcing it
into a plot type would park it on a page section away from its source.

Vendors mattpocock/skills' grilling verbatim (MIT, LICENSE.txt alongside),
which the skill invokes at the gate and at the maths. Both it and
/new-plot-type join the CLAUDE.md skills table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alexisj-inria and others added 4 commits August 28, 2026 11:25
The package had two answers to "how does this plot type behave?". Rendering
was pushed — a builder installs a RenderSpec on the Signal it constructs, so
to_plotly_trace never reaches for a plot type's module. Capabilities were
pulled — signal_container held a plot_type string and asked the registry,
`if self.plot_type in plot_types.GRID_LAYOUT`, in eight places.

The pull was justified by an import cycle: signal_container is reachable
from a half-initialised datasource package, so a plot.py importing Signal
back out of it would fail for some entry points and not others. That fear
drove the schema.py/plot.py split, the registry.py/builders.py split, a
find_spec probe instead of an import, and RenderSpec itself.

Tested rather than assumed: making registry import all three plot.py modules
at module scope broke no entry point. Two things absorbed it — the eager
`from clinical_scope.wrapper import ...` in __init__.py fixes module order
for every entry point, and signal_container bound the registry as a *module
object*, touching attributes only at call time. Real on the dependency
graph, inert in practice, and it had grown a second pattern for a problem
RenderSpec already solved.

The cost was never the cycle. It was that one question had two shapes, and
the string-keyed one accumulated hand-maintained duplicates: square_plot was
a second spelling of GRID_LAYOUT, kept in sync by nothing — the fake plot
type in the test suite declared one and not the other and rendered wrong.

So a Signal now carries the schema class itself. plot_options.schema and
PlotModel.schema hold type[PlotTypeSchema]; plot_type survives as a
read-only property returning schema.NAME, for logs, figure titles and Dash
stores, which is why every `model.plot_type == "loop"` read is untouched.
signal_container imports nothing from plot_types but base, asserted by
test_boundaries.

Two things could not simply be pushed:

- page order is a fact about the collection, not about one type, so
  assign_plot_model left PlotModel and became plot_assembly.
  assemble_plot_models — the module named for assembly now does both steps
- a plot type that crossed a Dash store as JSON has only its name left, so
  registry.schema_for is the single string-to-schema conversion. An
  unregistered name resolves to Unknown, every capability off; deliberately
  not the time_series defaults, which would let a typo render plausibly

That deletes the six capability frozensets, the globals() reflection check
that policed them, square_plot on both PlotOptions and PlotModel, and the
find_spec probe — builders.py already checks the same thing, keyed by the
schema rather than by a path.

The schema.py/plot.py and registry.py/builders.py splits stay, re-justified.
The cycle argument is dead but a better one survives: schema.py imports only
validation, so checking a config never loads numpy and plotly. "The config
layer must not import the render layer" is a rule a reader can hold; the
import-order argument it replaces is not.

Alongside, from the same review:

- data_callbacks read plot_model.name for RESAMPLED while reading
  plot_model.plot_type for POINT_TIMESTAMPS. They agree only because
  __post_init__ aliases name to the plot type, and name is also a Dash
  component id, so a real title would have silently broken resampling
- spectrogram and psd returned no issue for a non-dict entry, which is the
  validates-cleanly-renders-nothing failure the package exists to kill; loop
  already reported it properly
- Data.loop_time_axis is now point_time_axis, matching the POINT_TIMESTAMPS
  capability that gates it — data_callbacks reads it inside a branch on that
  generic flag, so a second timestamped type would have had to populate a
  field named after loops
- the schema/builder seam was typed Any throughout. PlotBuilder.build and
  read_sheet now have real signatures behind `if TYPE_CHECKING`, which never
  executes and so closes no cycle. entries/entry/config stay Any and say so:
  they are raw user JSON, and narrowing them is validate()'s job
- CellReader gains text() and pair(), absorbing fifteen str(row.get()).strip()
  copies and the db_min/db_max rule that was written twice with two different
  messages. The row loop is left alone — psd's rows fan out into several
  groups, so a shared template would have one and a half users
- ValidationIssue.unknown_keys replaces six hand-rolled copies of one message

A regression test covers the square_plot bug directly: a grid type with a
single subplot must still get a figure width, from GRID_LAYOUT and nothing
else. The fake plot type is where it belongs, since that is the type that
had it wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
registry.py and builders.py were one roster split in two: the first held
every schema, the second held what builds each. They were separated because
a plot.py imports signal_container, which imported registry — folding them
together would have closed a cycle.

That cycle is gone. Nothing outside plot_types reaches the registry for a
capability any more, so signal_container imports only plot_types.base, and
registry is free to import the render halves it is a roster of.

The justification the split had left was that importing the config layer
would no longer be cheap. Measured, and it was already untrue:

    >>> import clinical_scope.database_options_parser
    heavy modules pulled in: ['plotly', 'numpy']
    plot.py modules loaded : [loop.plot, psd.plot, spectrogram.plot]

clinical_scope/__init__.py eagerly imports wrapper, so every plot.py is
loaded before the parser's first line runs. No entry point ever saw the
cheap config layer the split was protecting.

So registering a plot type is now two adjacent lines in one file, AVAILABLE
and BUILDERS, and the two completeness checks became one: a derived type
with no builder raises from the same place a duplicate name does, rather
than from a second module the first knows nothing about.

BUILDERS stays keyed by the schema class rather than by name, so a builder
cannot be filed under a spelling no type answers to.

The plot modules are imported as _loop_plot rather than _loop: the alias
names a module inside the package, not the package, and _loop.BUILDER read
as though the loop package exported one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Schema" described one third of what the file holds. Alongside the config
grammar — validate, map_refs, read_sheet — it carries the type's identity
(NAME, SECTION_KEY) and its six capability flags, which are booleans about
how the type *draws* and are the file's most-read content. A file named for
configuration was the wrong place to look for them, which is part of why
"may signal_container import this?" stayed confusing for so long.

The word was also already spent twice. cst.UserOptions.LoopsPerRow and its
siblings are called schema classes, and data_callbacks builds a Dash
"schema-registry" mapping component ids to widget schemas — genuinely a
schema, and unrelated. Three meanings, and the plot type had the weakest
claim on the word.

So: schema.py becomes definition.py, PlotTypeSchema becomes
PlotTypeDefinition, plot_options.schema becomes plot_options.definition, and
registry.schema_for becomes definition_for. It reads correctly at the use
site — self.definition.GRID_LAYOUT is "this plot type's definition says it
is a grid" — where self.schema.GRID_LAYOUT read as a category error.

Mechanical, but not blindly: the other two meanings were left alone, and
several sentences the rename would have degraded were rewritten rather than
carried over. "The sheet columns and the JSON keys are one schema in two
spellings" was right the first time and became "one grammar in two
spellings"; "the parser reaches every schema" became "every plot type".

The files stay split. The layering reason is gone — the previous commit
showed the config half is never loaded alone — but psd is 302 lines of
spreadsheet row accumulation and group resolution against 154 lines of a
Welch call and axis wiring, and someone asking why a row did not become a
plot is not the person asking why a PSD is scaled wrong. It also keeps the
shape a registered module already has here: datasource/sources/<name>/ is
options.py plus find_load_format.py, the same declarative half and working
half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AlexisJanin
AlexisJanin merged commit c06cecc into main Aug 28, 2026
3 checks passed
@AlexisJanin
AlexisJanin deleted the plot-types-package branch August 31, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants